package cn.accjiyun.cli.common;


import org.apache.commons.cli.*;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

/**
 * Created by jiyun on 2017/11/2.
 */
public class CommandLineHelper {
    private Class<?> targetClass = null;
    private Options cmdLineOptions = null;
    private Set<Option> requiredOptions = new HashSet<>();
    private HashMap<String, Class<?>> typeMap
            = new HashMap<>();
    private HashMap<String, Object> parsedArgs = new HashMap<>();
    private CommandLine commandLine = null;

    /**
     * 创建对应命令行类
     * @param thisClass
     * @throws Exception
     */
    public CommandLineHelper(Class<?> thisClass) throws Exception {
        targetClass = thisClass;
        cmdLineOptions = buildOptions();
    }

    /**
     * 初始化命令行类注解中的命令属性
     * @return 命令行选项
     * @throws Exception
     */
    private Options buildOptions() throws Exception {
        Options opts = new Options();
        CLI copts = null;
        for (Annotation annotation : targetClass.getAnnotations()) {
            if (!(annotation instanceof CLI)) continue;
            copts = (CLI) annotation;
            break;
        }
        if (copts != null) {
            for (Option copt : copts.value()) {
                if (copt.longOpt().equals(""))
                    opts.addOption(copt.opt(), copt.hasArg(), copt.description());
                else
                    opts.addOption(copt.opt(), copt.longOpt(), copt.hasArg(), copt.description());
                if (copt.required()) {
                    requiredOptions.add(copt);
                }
                typeMap.put(copt.opt(), copt.type());
                try {
                    parsedArgs.put(copt.opt(),
                            parseObjectFromString(copt.defaultValue(), copt.type()));
                } catch (Exception e) {
                    // This should never happen unless someone misused the API
                    // typically happens when type is specified but not defaultValue
                    throw new Exception("Bad usage of CLI API: defaultValue for option '" +
                            copt.opt() + "' does not match the type specified");
                }
            }
        }
        return opts;
    }

    /**
     * 打印帮助提示
     */
    public void displayHelp() {
        new HelpFormatter().printHelp(targetClass.getSimpleName(), cmdLineOptions);
        System.out.flush();
    }

    public boolean parse(String[] args) {
        try {
            if (args.length == 0 && requiredOptions.size() != 0) {
                StringBuilder message = new StringBuilder("Missing command line arguments: ");
                for (Option o : requiredOptions) {
                    message.append("-" + o.longOpt() + " ");
                }
                System.out.println(message);
                displayHelp();
            } else {
                commandLine =
                        checkSanity(new DefaultParser().parse(cmdLineOptions, args));
                if (commandLine != null) return true;
            }
        } catch (ParseException e) {
            System.err.println("Command syntax error: " + e.getMessage());
            displayHelp();
        }
        return false;
    }

    public CommandLine getCommandLine() {
        return commandLine;
    }

    /**
     * 检查各个参数输入是否符合要求
     * @param cmd
     * @return
     */
    private CommandLine checkSanity(CommandLine cmd) {
        // Check for a help flag.
        if (cmd.hasOption("help") || cmd.hasOption("h")) {
            displayHelp();
            return null;
        }

        // check for required option
        StringBuilder message = new StringBuilder("Required command line argument missing: ");
        for (Option option : requiredOptions) {
            if (!cmd.hasOption(option.opt()) && !cmd.hasOption(option.longOpt())) {
                message.append("-" + option.longOpt() + " ");
                System.out.println("Required command line argument missing: " + option);
            }
        }

        // iterate through all options with args and check for types
        // TODO: We're currently parsing only typed objects Fix this
        for (String option : typeMap.keySet()) {
            if (!cmdLineOptions.getOption(option).hasArg()) continue;
            Class<?> expectedType = typeMap.get(option);
            if (cmd.hasOption(option)) {
                try {
                    Object argument =
                            parseObjectFromString(cmd.getOptionValue(option), expectedType);
                    // failure to parse will cause exception to be thrown
                    // if no exceptions then we've type-checked!
                    parsedArgs.put(option, argument);
                } catch (Exception e) {
                    System.err.println("Argument type violation for option \"-" +
                            option + "\", the type should be : " + expectedType.getSimpleName());
                }
            }
        }
        return cmd;
    }

    public String[] getUnparsedArgs() {
        if (commandLine == null) return null;
        return commandLine.getArgs();
    }

    /**
     * 获取参数对应的值
     * @param option 参数选项
     * @return 对应值
     */
    public Object getOptionValue(String option) {
        if (!parsedArgs.containsKey(option)) return null;
        return parsedArgs.get(option);
    }

    /**
     * 利用反射创建对象
     * @param s 构造函数参数
     * @param clazz 类
     * @param <T>
     * @return
     * @throws Exception
     */
    private static <T> T parseObjectFromString(
            String s, Class<T> clazz) throws Exception {
        return clazz.getConstructor(new Class[]{String.class}).newInstance(s);
    }
}
